const std = @import("std");
const fs = std.fs;

const Indent = "    ";

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();

    const allocator = gpa.allocator();
    const cmd_param = try CmdParameter.Get(allocator);
    defer allocator.free(cmd_param.output_dir);
    defer allocator.free(cmd_param.target_dir);

    var dir = try fs.openDirAbsolute(cmd_param.target_dir, .{ .access_sub_paths = false, .iterate = true });
    defer dir.close();

    const output_file = try std.fmt.allocPrint(allocator, "{s}{s}{s}", .{ cmd_param.output_dir, fs.path.sep_str, fs.path.basename(cmd_param.target_dir) });
    defer allocator.free(output_file);
    if (cmd_param.compare) {
        const result_file = try std.fmt.allocPrint(allocator, "{s}{s}{s}_result", .{ cmd_param.output_dir, fs.path.sep_str, fs.path.basename(cmd_param.target_dir) });
        defer allocator.free(result_file);

        try DirCompare(&dir, &output_file, &result_file, allocator);
    } else {
        try InitDirTracker(&dir, &output_file, allocator);
    }
}

fn DirCompare(dir: *fs.Dir, output_file: *const []u8, result_file: *const []u8, allocator: std.mem.Allocator) !void {
    var list = std.ArrayList([]u8).init(allocator);
    defer list.deinit();
    defer for (list.items) |item| {
        allocator.free(item);
    };

    try SetupTrackerInfo(output_file, &list, allocator);

    const item_size: usize = list.items.len;
    var path_exist = try allocator.alloc(bool, item_size);
    defer allocator.free(path_exist);
    try DoCompare(dir, output_file, result_file, allocator, &list, &path_exist);
}

fn DoCompare(dir: *fs.Dir, output_file: *const []u8, result_file: *const []u8, allocator: std.mem.Allocator, list: *std.ArrayList([]u8), path_exist: *[]bool) !void {
    var tracker = try fs.createFileAbsolute(output_file.*, .{ .truncate = true });
    defer tracker.close();

    var tracker_buffered_writer = std.io.bufferedWriter(tracker.writer());
    const tracker_writer = tracker_buffered_writer.writer();

    var result = try fs.createFileAbsolute(result_file.*, .{ .truncate = true });
    defer result.close();

    var result_buffered_writer = std.io.bufferedWriter(result.writer());
    const result_writer = result_buffered_writer.writer();

    const parent_dir = try std.fmt.allocPrint(allocator, ".{s}", .{fs.path.sep_str});
    defer allocator.free(parent_dir);

    try WalkDir(dir, tracker_writer, result_writer, &parent_dir, allocator, list, path_exist);

    for (list.items, 0..) |item, i| {
        if (!path_exist.*[i]) {
            try result_writer.print("Del {s}\n", .{item});
        }
    }

    try tracker_buffered_writer.flush();
    try result_buffered_writer.flush();
}

fn WalkDir(dir: *fs.Dir, tracker_writer: anytype, result_writer: anytype, parent_dir: *const []u8, allocator: std.mem.Allocator, list: *std.ArrayList([]u8), path_exist: *[]bool) !void {
    var iterate = dir.iterate();
    while (try iterate.next()) |entry| {
        const path = try std.fmt.allocPrint(allocator, "{s}{s}", .{ parent_dir.*, entry.name });
        defer allocator.free(path);

        try tracker_writer.print("{s}\n", .{path});

        var exist = false;
        for (list.*.items, 0..) |item, i| {
            if (std.mem.eql(u8, item, path)) {
                path_exist.*[i] = true;
                exist = true;
                break;
            }
        }
        if (!exist) {
            try result_writer.print("Add {s}\n", .{path});
        }

        switch (entry.kind) {
            fs.File.Kind.directory => {
                const sub_parent_dir = try std.fmt.allocPrint(allocator, "{s}{s}{s}", .{ parent_dir.*, entry.name, fs.path.sep_str });
                defer allocator.free(sub_parent_dir);
                var subDir = try dir.openDir(entry.name, .{ .access_sub_paths = false, .iterate = true });
                defer subDir.close();
                try WalkDir(&subDir, tracker_writer, result_writer, &sub_parent_dir, allocator, list, path_exist);
            },
            else => {},
        }
    }
}

fn SetupTrackerInfo(output_file: *const []u8, list: *std.ArrayList([]u8), allocator: std.mem.Allocator) !void {
    var tracker = try fs.openFileAbsolute(output_file.*, .{ .mode = fs.File.OpenMode.read_only });
    var buf_reader = std.io.bufferedReader(tracker.reader());
    var in_stream = buf_reader.reader();
    var line_buffer: [2048]u8 = undefined;

    while (try in_stream.readUntilDelimiterOrEof(&line_buffer, '\n')) |line| {
        try list.*.append(try allocator.dupe(u8, line));
    }
}

fn InitDirTracker(dir: *fs.Dir, output_file: *const []u8, allocator: std.mem.Allocator) !void {
    var tracker = try fs.createFileAbsolute(output_file.*, .{ .truncate = true });
    defer tracker.close();

    var buffered_writer = std.io.bufferedWriter(tracker.writer());
    const writer = buffered_writer.writer();

    const parent_dir = try std.fmt.allocPrint(allocator, ".{s}", .{fs.path.sep_str});
    defer allocator.free(parent_dir);

    try PrintDir(dir, writer, &parent_dir, allocator);

    try buffered_writer.flush();
}

fn PrintDir(dir: *fs.Dir, writer: anytype, parent_dir: *const []u8, allocator: std.mem.Allocator) !void {
    var iterate = dir.iterate();
    while (try iterate.next()) |entry| {
        switch (entry.kind) {
            fs.File.Kind.file => {
                try writer.print("{s}{s}\n", .{ parent_dir.*, entry.name });
            },
            fs.File.Kind.directory => {
                try writer.print("{s}{s}\n", .{ parent_dir.*, entry.name });
                const sub_parent_dir = try std.fmt.allocPrint(allocator, "{s}{s}{s}", .{ parent_dir.*, entry.name, fs.path.sep_str });
                defer allocator.free(sub_parent_dir);
                var subDir = try dir.openDir(entry.name, .{ .access_sub_paths = false, .iterate = true });
                defer subDir.close();
                try PrintDir(&subDir, writer, &sub_parent_dir, allocator);
            },
            else => {},
        }
    }
}

const CmdParameter = struct {
    target_dir: []const u8,
    output_dir: []const u8,
    compare: bool,

    pub fn Get(allocator: std.mem.Allocator) !@This() {
        var args = try std.process.argsWithAllocator(allocator);
        defer args.deinit();

        _ = args.skip();

        const target_dir: []const u8 = args.next() orelse return error.MissingTargetDir;
        const output_dir: []const u8 = args.next() orelse return error.MissingOutputDir;
        var compare = false;
        if (args.next()) |arg| {
            compare = std.mem.eql(u8, "--compare", arg);
        }

        return CmdParameter{ .target_dir = try allocator.dupe(u8, target_dir), .output_dir = try allocator.dupe(u8, output_dir), .compare = compare };
    }
};
